﻿using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using SlamCms.Common;
using SlamCms.Data;

namespace SlamCms.Web.Sitemaps
{
	public class Sitemap
	{
		private SitemapNode _rootNode;

		protected virtual void OnCreating(SitemapBuilder builder)
		{
		}

		public string SitemapId { get; protected set; }
		public string Title { get; protected set; }

		public SitemapNode RootNode 
		{
			get
			{
				return this._rootNode; 
			}

			protected set
			{
				Ensure.PropertyNotNull(value);

				this._rootNode = value;
			}
		}

		public SitemapNode FindNode(string key)
		{
			Ensure.ArgumentNotNull(key, "key");

			if (RootNode == null)
				throw new InvalidOperationException("The sitemap doesn't have a root node");

			return FindNode( n => n.Key == key, RootNode);
		}

		public SitemapNode FindNode(Predicate<SitemapNode> evaluator, SitemapNode startingNode = null)
		{
			Ensure.ArgumentNotNull(evaluator, "evaluator", "Must specify an evaluator function");

			if (startingNode == null)
				startingNode = RootNode;

			var result = this.FindNodeInner(evaluator, startingNode);

			if (result == null)
			{ 
				// TODO node not found, regenerate sitemap ?
			}

			return result;
		}

		public IEnumerable<SitemapNode> FindNodes(Predicate<SitemapNode> evaluator, SitemapNode startingNode = null)
		{
			Ensure.ArgumentNotNull(evaluator, "evaluator", "Must specify an evaluator function");

			if (startingNode == null)
				startingNode = RootNode;

			return FindNodesInner(evaluator, startingNode);
		}

		protected SitemapNode FindNodeInner(Predicate<SitemapNode> evaluator, SitemapNode startingNode)
		{
			Ensure.ArgumentNotNull(evaluator, "evaluator", "Must specify an evaluator function");
			Ensure.ArgumentNotNull(startingNode, "startingNode", "Must specify a starting node");
			
			if (evaluator.Invoke(startingNode))
				return startingNode;

			foreach (var child in startingNode.Children)
			{
				var result = FindNodeInner(evaluator, child);

				if (result != null)
					return result;
			}

			return null;
		}

		protected IEnumerable<SitemapNode> FindNodesInner(Predicate<SitemapNode> evaluator, SitemapNode startingNode)
		{
			Ensure.ArgumentNotNull(startingNode, "startingNode", "Must specify a starting node");
			Ensure.ArgumentNotNull(evaluator, "evaluator", "Must specify an evaluator function");

			if (evaluator.Invoke(startingNode))
				yield return startingNode;

			foreach (var child in startingNode.Children)
			{
				foreach (var result in FindNodesInner(evaluator, child))
					yield return result;
			}
		}

		protected void BuildSitemap(SitemapBuilder builder)
		{
			var dynamicNodeGroups = builder.Nodes.GroupBy(x => x.ParentKey ?? string.Empty).ToList();

			// first the root nodes
			var rootNodesGroup = dynamicNodeGroups.FirstOrDefault(n => n.Key.IsNullOrEmpty());

			if (rootNodesGroup == null)
				return;

			var rootNodes = rootNodesGroup.OrderBy(x => x.Order).ThenBy(x => x.Title);
			var rootNode = rootNodes.First();

			_rootNode = GetSitemapNode(null, rootNode);

			if (rootNodes.Count() == 1)
			{
				AddChildren(_rootNode, dynamicNodeGroups);
			}
			else
			{
				// more than one root node, so use a copy of the first root node as root
				foreach (var dynamicNode in rootNodes)
				{
					var sitemapNode = GetSitemapNode(_rootNode, dynamicNode);
					_rootNode.AddChild(sitemapNode);
					AddChildren(sitemapNode, dynamicNodeGroups);
				}
			}
		}

		protected void AddChildren(SitemapNode parentNode, List<IGrouping<string, DynamicSitemapNode>> dynamicNodeGroups)
		{
			var childrenGroup = dynamicNodeGroups.FirstOrDefault(x => x.Key.Equals(parentNode.Key, StringComparison.InvariantCultureIgnoreCase));

			if (childrenGroup == null)
				return;

			var children = childrenGroup.OrderBy(x => x.Order).ThenBy(x => x.Title);

			foreach (var child in children)
			{
				var sitemapNode = GetSitemapNode(parentNode, child);
				parentNode.AddChild(sitemapNode);
				AddChildren(sitemapNode, dynamicNodeGroups);
			}
		}

		protected SitemapNode GetSitemapNode(SitemapNode parentNode, DynamicSitemapNode dynamicNode)
		{
			return new SitemapNode()
			{
				Key = dynamicNode.Key,
				Title = dynamicNode.Title,
				Properties = dynamicNode.Properties,
				Url = dynamicNode.Url,
				Parent = parentNode
			};
		}

		internal static T Build<T>(DataContext dataContext, string sitemapId) where T : Sitemap
		{
			var instance = Activator.CreateInstance<T>();

			// try to get info from the DB
			var dbSitemap = dataContext.Database.GetSitemap(sitemapId);

			// if it's deriving from Sitemap it doesn't need to be in the DB
			if (typeof(T) == typeof(Sitemap) && dbSitemap == null)
				throw new Exception("Could not find sitemap {0}".F(sitemapId));

			instance.SitemapId = sitemapId;
			
			IEnumerable<DynamicSitemapNode> initialNodes = new List<DynamicSitemapNode>();

			if (dbSitemap != null)
			{
				instance.Title = dbSitemap.Title;
				initialNodes = GetInitialDynamicNodes(dbSitemap);
			}

			var builder = new SitemapBuilder(dataContext, initialNodes);

			instance.OnCreating(builder);

			instance.BuildSitemap(builder);

			return instance;
		}

		private static IEnumerable<DynamicSitemapNode> GetInitialDynamicNodes(SlamCms.Data.Internal.Sitemap sitemap)
		{
			return sitemap.Nodes.Select(x => new DynamicSitemapNode()
			{
				Key = "n{0}".F(x.PositionId),
				ParentKey = x.ParentPositionId == null ? null : "n{0}".F(x.ParentPositionId),
				Title = x.Title,
				Url = x.Url,
				Properties = x.Properties != null ? JsonConvert.DeserializeObject<Dictionary<string, object>>(x.Properties) : null,
				Order = x.Order
			});
		}
	}
}